package gm.data;

import java.awt.Color;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import org.apache.commons.io.IOUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

/**
 * Kanava alaposztálya.
 *
 * @author imruf84
 */
public class Kanava {

    /**
     * Alapértelmezett kódolás mentéskor.
     */
    private static final String ENCODING = "UTF-8";
    /**
     * Szöveges tömörítetlen fájlkiterjesztés.
     */
    public static final String UNCOMPRESSED_TEXT_FILE_EXTENSION = ".GMT";
    /**
     * Bináris tömörített fájlkiterjesztés.
     */
    public static final String COMPRESSED_BINARY_FILE_EXTENSION = ".GMB";
    /**
     * Kanava módosult.
     */
    private boolean modified = true;
    /**
     * Fájlnév elérési úttal.
     */
    private String filePathName = null;
    /**
     * Szkript forráskódja.
     */
    private String script = "";
    /**
     * Sorok száma.
     */
    private int rowsCount = 0;
    /**
     * Oszlopok száma.
     */
    private int columnsCount = 0;
    /**
     * Lyukak tárolója.
     */
    private HashMap<Integer, HashMap<Integer, Hole>> holes = null;
    /**
     * Öltések tárolója.
     */
    private HashMap<Integer, HashMap<Integer, Stitch>> stitches = null;
    /**
     * Színek tárolója.
     */
    private HashMap<Character, HoleColor> colors = null;
    /**
     * Fonalak tárolója.
     */
    private HashMap<Character, Wool> wools = null;

    /**
     * Konstruktor.
     *
     * @param rows sorok száma
     * @param columns oszlopok száma
     * @param create igaz esetén el is készíti a lyukakat
     */
    public Kanava(int rows, int columns, boolean create) {
        this.holes = new HashMap<>();
        this.stitches = new HashMap<>();
        this.wools = new HashMap<>();
        this.colors = new HashMap<>();
        this.colors.put(' ', new HoleColor(Color.WHITE, ' '));
        this.rowsCount = rows;
        this.columnsCount = columns;

        if (create) {
            this.createKanava();
        }
    }

    /**
     * Kanava létrehozása.
     */
    public final void createKanava() {

        // Lyukak és öltések létrehozása.
        int all = this.rowsCount * this.columnsCount;
        int created = 0;
        for (int r = 0; r < this.rowsCount; r++) {

            HashMap<Integer, Hole> rowHole = new HashMap<>();
            holes.put(r, rowHole);
            HashMap<Integer, Stitch> rowStitch = new HashMap<>();
            if (r > 0) {
                stitches.put(r - 1, rowStitch);
            }

            for (int c = 0; c < this.columnsCount; c++) {
                // Lyuk tárolása.
                rowHole.put(c,
                        new Hole(r, c,
                                this.getHole(r - 1, c),
                                this.getHole(r + 1, c),
                                this.getHole(r, c - 1),
                                this.getHole(r, c + 1),
                                this.getHoleColor(' ')));

                // Öltés tárolása.
                if ((r > 0) && (c > 0)) {
                    Hole hole = this.getHole(r, c);
                    rowStitch.put(c - 1,
                            new Stitch(r - 1, c - 1,
                                    hole.left(),
                                    hole.top()));
                }

                // Esemény futtatása.
                if (!this.updateCreation(all, ++created)) {
                    return;
                }
            }
        }

    }

    /**
     * Lyuk színének lekérdezése szimbólum alapján.
     *
     * @param symbol szimbólum
     * @return lyuk szín objektuma
     */
    public HoleColor getHoleColor(char symbol) {
        return this.colors.get(symbol);
    }

    /**
     * Színek szimbólumainak a lekérdezése.
     *
     * @return szimbólumok tömbje
     */
    public Object[] getHoleColorSymbols() {

        Object[] k = this.colors.keySet().toArray();

        Arrays.sort(k, (Object t, Object t1) -> {
            int c = t1.toString().compareToIgnoreCase(t.toString());
            if (0 == c) {
                return Character.isUpperCase((Character) t1) ? 1 : -1;
            }
            
            return c;
        });

        return k;
    }

    /**
     * Szín hozzáadása.
     *
     * @param c szín
     * @return szín
     */
    public HoleColor addHoleColor(HoleColor c) {

        // Szín tárolása ha még nem létezne.
        if (this.colors.containsKey(c.getSymbol())) {
            // Egyébként csak módosítjuk.
            this.colors.get(c.getSymbol()).setColor(c.getColor());
        } else {
            this.colors.put(c.getSymbol(), c);
        }

        // Megfelelő színű fonal hozzáadása.
        this.addWool(c);

        // Kanava módosult.
        this.toModified();

        return this.colors.get(c.getSymbol());
    }

    /**
     * Szín eltávolítása.
     *
     * @param symbol szín szimbóluma
     * @return igaz ha a törlés megtörtént.
     */
    public boolean removeHoleColor(Character symbol) {
        // Az alapszín nem törölhető.
        if (' ' == symbol) {
            return false;
        }

        // Az ilyen színű lyukak eltávolítása.
        for (int row = 0; row < this.getRowsCount(); row++) {
            for (int col = 0; col < this.getColumnsCount(); col++) {
                Hole hole = this.getHole(row, col);
                if (hole.getHoleColor().getSymbol() == symbol) {
                    hole.setHoleColor(this.getHoleColor(' '));
                }
            }
        }

        // Szín törlése.
        this.colors.remove(symbol);

        // Adott színű fonal törlése.
        this.removeWool(symbol);

        // Kanava módosult.
        this.toModified();

        return true;
    }

    /**
     * Fonal hozzáadása.
     *
     * @param symbol szimbólum
     */
    private void addWool(HoleColor color) {
        // Háttérszínű fonalat nem tárolunk.
        if (' ' == color.getSymbol()) {
            return;
        }

        // Fonal lekérdezése.
        Wool w = this.wools.get(color.getSymbol());

        // Ha még nincs ilyen fonal, akkor tároljuk.
        if (null == w) {
            this.wools.put(color.getSymbol(), new Wool(color));
            return;
        }

        // Egyébként csak a színt módosítjuk.
        w.setColor(color);

    }

    /**
     * Fonal törlése.
     *
     * @param symbol szimbólum.
     */
    private void removeWool(char symbol) {
        Wool w = this.getWool(symbol);
        Object o[] = w.toArray();
        for (Object so : o) {
            Stitch s = (Stitch) so;
            s.resetReversion();
            s.removeFromWool();
        }

        this.wools.remove(symbol);
    }

    /**
     * Fonal lekérdezése.
     *
     * @param symbol szimbólum
     * @return fonal
     */
    public Wool getWool(char symbol) {
        return this.wools.get(symbol);
    }

    /**
     * Létrehozás minden szakaszában lefutó esemény.
     *
     * @param all összesen létrehozandó cellák száma
     * @param created létrehozott cellák száma
     * @return hamis esetén leáll a létrehozás
     */
    public boolean updateCreation(int all, int created) {
        return true;
    }

    /**
     * Sorok számának a lekérdezése.
     *
     * @return sorok száma
     */
    public int getRowsCount() {
        return this.rowsCount;
    }

    /**
     * oszlopok számának a lekérdezése.
     *
     * @return oszlopok száma
     */
    public int getColumnsCount() {
        return this.columnsCount;
    }

    /**
     * Lyuk lekérdezése.
     *
     * @param r sorszám
     * @param c oszlopszám
     * @return lyuk
     */
    public final Hole getHole(int r, int c) {

        return (null == this.holes.get(r) ? null : this.holes.get(r).get(c));
    }

    /**
     * Öltés lekérdezése.
     *
     * @param r sorszám
     * @param c oszlopszám
     * @return öltés
     */
    public Stitch getStitch(int r, int c) {
        return (null == this.stitches.get(r) ? null : this.stitches.get(r).get(c));
    }

    /**
     * Kanava móduslásának lekérdezése.
     *
     * @return módosított kanava esetén igaz
     */
    public boolean isModified() {
        return this.modified;
    }

    /**
     * Kanava módosult.
     */
    public void toModified() {
        this.modified = true;
    }

    /**
     * Kanava módosítottságának a megadása.
     *
     * @param m módosítottság
     */
    public void setModified(boolean m) {
        this.modified = m;
    }

    /**
     * Egyszerű fájlnév lekérdezése.
     *
     * @return egyszerű fájlnév
     */
    public String getFileSimpleName() {

        if (null == this.filePathName) {
            return "";
        }

        return new File(this.filePathName).getName();
    }

    /**
     * Mentés szükségességének lekérdezése.
     *
     * @return igaz ha mentés szükséges, egyébként hamis
     */
    public boolean saveNeeded() {
        return this.isModified();
    }

    /**
     * Kanava törlése.
     */
    public void clearKanava() {
        this.holes.clear();
        Object hca[] = this.colors.values().toArray();
        for (Object hco : hca) {
            this.removeHoleColor(((HoleColor) hco).getSymbol());
        }
        this.colors.put(' ', new HoleColor(Color.WHITE, ' '));
        this.rowsCount = 0;
        this.columnsCount = 0;
    }

    /**
     * Fájlkiterjesztés lekérdezése.
     *
     * @param path elérési út
     * @return fájlkiterjesztés
     */
    private static String getFileExtension(String path) {

        // Ha nem tartalmaz pontot akkor hibával kilépünk.
        if (0 > path.indexOf(".")) {
            return "";
        }

        return path.substring(path.lastIndexOf('.'));
    }

    /**
     * Karakterlánc tömörítése.
     *
     * @param str karakterlánc
     * @return tömörített karakterlánc bájtkódként
     * @throws IOException hiba kivétel
     */
    private static byte[] compress(String str) throws IOException {
        int size = 1024;
        ByteArrayOutputStream baos;
        GZIPOutputStream gzip;
        try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(str.getBytes(ENCODING)))) {
            baos = new ByteArrayOutputStream();
            gzip = new GZIPOutputStream(baos);
            byte[] buffer = new byte[size];
            int len;
            while ((len = bis.read(buffer, 0, size)) != -1) {
                gzip.write(buffer, 0, len);
            }
            gzip.finish();
        }
        gzip.close();
        return baos.toByteArray();
    }

    /**
     * Bájttömb kicsomagolása karakterlánccá.
     *
     * @param data bájttömb
     * @return kicsomagolt bájtkód karakterláncként
     * @throws IOException hiba kivétel
     */
    private static String uncompress(byte[] data) throws IOException {
        int size = 1024;
        ByteArrayOutputStream baos;
        try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(data))) {
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[size];
            int len;
            while ((len = gzip.read(buffer, 0, size)) != -1) {
                baos.write(buffer, 0, len);
            }
        }
        baos.close();
        return new String(baos.toByteArray(), ENCODING);
    }

    /**
     * Kanava tárolása fájlba.
     *
     * @param script szkript forráskódja
     * @see <a
     * href="https://code.google.com/p/json-simple/wiki/EncodingExamples">https://code.google.com/p/json-simple/wiki/EncodingExamples</a>
     * @return fájlnév létezése és állomány mentése esetén igaz egyébként hamis
     * @throws java.io.IOException fájl sikertelen mentése
     */
    @SuppressWarnings("unchecked")
    public boolean save(String script) throws IOException {
        // Ha nincs még filenév akkor nem tudunk menteni.
        if (null == this.filePathName) {
            return false;
        }

        // Szkript tárolása.
        this.setScriptSourceCode(script);

        // Mentés elvégzése.
        JSONObject jsonRoot = new JSONObject();
        // Szkript.
        jsonRoot.put("script", this.getScriptSourceCode());
        // Méret.
        jsonRoot.put("rc", this.getRowsCount());
        jsonRoot.put("cc", this.getColumnsCount());
        // Szkript.
        jsonRoot.put("script", script);
        // Színek.
        JSONArray jsonColors = new JSONArray();
        jsonRoot.put("c", jsonColors);
        this.colors.keySet().stream().map((c) -> getHoleColor(c)).map((hc) -> {
            JSONObject jsonColor = new JSONObject();
            jsonColor.put("s", Character.toString(hc.getSymbol()));
            jsonColor.put("r", hc.getColor().getRed());
            jsonColor.put("g", hc.getColor().getGreen());
            jsonColor.put("b", hc.getColor().getBlue());
            return jsonColor;
        }).forEach((jsonColor) -> {
            jsonColors.add(jsonColor);
        });
        // Lyukak.
        JSONArray jsonHoles = new JSONArray();
        jsonRoot.put("h", jsonHoles);
        this.holes.values().stream().forEach((hm) -> {
            hm.values().stream().filter((h) -> !(' ' == h.getHoleColor().getSymbol())).map((h) -> {
                // A háttérszínt nem mentjük.
                JSONObject jsonHole = new JSONObject();
                jsonHole.put("r", h.getRow());
                jsonHole.put("c", h.getColumn());
                jsonHole.put("s", Character.toString(h.getHoleColor().getSymbol()));
                return jsonHole;
            }).forEach((jsonHole) -> {
                jsonHoles.add(jsonHole);
            });
        });

        // Fonalak.
        JSONArray jsonWools = new JSONArray();
        jsonRoot.put("w", jsonWools);
        this.wools.values().stream().forEach((w) -> {
            JSONObject jsonWool = new JSONObject();
            jsonWools.add(jsonWool);
            JSONArray jsonStitches = new JSONArray();
            jsonWool.put("c", Character.toString(w.getColor().getSymbol()));
            jsonWool.put("s", jsonStitches);
            Iterator<Stitch> it = w.iterator();
            while (it.hasNext()) {
                Stitch s = it.next();
                JSONObject jsonStitch = new JSONObject();
                jsonStitch.put("r", s.getRow());
                jsonStitch.put("c", s.getColumn());
                if (s.isReversed()) {
                    jsonStitch.put("i", 1);
                }
                jsonStitches.add(jsonStitch);
            }
        });

        // Fájl kiterjesztésének a lekérdezése.
        String extension = getFileExtension(this.filePathName);

        // Adatok átalakítása karakterlánccá.
        String dataToSave = JSONUtils.JSONToString(jsonRoot);

        // Alapesetben tömörítünk...
        if (extension.equalsIgnoreCase(COMPRESSED_BINARY_FILE_EXTENSION)) {
            try (FileOutputStream stream = new FileOutputStream(this.filePathName)) {
                stream.write(compress(dataToSave));
                stream.close();
            }
        } else {
            // ...egyébként tömörítetlen szöveges formátumot használunk.
            try (BufferedWriter out = new BufferedWriter(new FileWriter(this.filePathName))) {
                out.write(dataToSave);
                out.close();
            }
        }

        this.modified = false;

        return true;
    }

    /**
     * Kanava tárolása fájlba adott fájlnéven.
     *
     * @param filePathName fájl neve és elérési útja
     * @param script szkript forráskódja
     * @throws java.io.IOException sikertelen mentés esetén
     */
    public void saveAs(String filePathName, String script) throws IOException {
        this.filePathName = filePathName;
        this.save(script);
    }

    /**
     * Kanava megnyitása fájlból.
     *
     * @param filePathName fájl neve elérési úttal
     * @throws java.io.FileNotFoundException ha a fájl nem található
     */
    @SuppressWarnings("unchecked")
    public void open(String filePathName) throws FileNotFoundException, IOException {

        // Fájlkiterjesztés lekérdezése.
        String extension = getFileExtension(filePathName);

        // Kanava "tisztítása".
        this.clearKanava();

        // Adatok tárolója.
        JSONObject jsonRoot;

        // Alapesetben tömörített fájlformátumot használunk...
        if (extension.equalsIgnoreCase(COMPRESSED_BINARY_FILE_EXTENSION)) {

            FileInputStream in = new FileInputStream(filePathName);
            byte[] bytes = IOUtils.toByteArray(in);

            String uc = uncompress(bytes);
            jsonRoot = (JSONObject) JSONValue.parse(uc);

        } else {
            // ...egyébként tömörítetlen formátumot.
            jsonRoot = JSONUtils.JSONFromFile(filePathName);
        }

        // Méret.
        this.rowsCount = ((Long) jsonRoot.get("rc")).intValue();
        this.columnsCount = ((Long) jsonRoot.get("cc")).intValue();

        // Kanava létrehozása.
        this.createKanava();

        // Szkript.
        this.setScriptSourceCode(((String) jsonRoot.get("script")));

        // Színek.
        JSONArray jsonColors = (JSONArray) jsonRoot.get("c");
        for (Iterator it = jsonColors.iterator(); it.hasNext();) {
            Object jsonColor1 = it.next();
            JSONObject jsonColor = (JSONObject) jsonColor1;
            Character symbol = ((String) jsonColor.get("s")).charAt(0);
            int r = ((Long) jsonColor.get("r")).intValue();
            int g = ((Long) jsonColor.get("g")).intValue();
            int b = ((Long) jsonColor.get("b")).intValue();
            this.addHoleColor(new HoleColor(new Color(r, g, b), symbol));
        }

        // Lyukak színezése.
        JSONArray jsonHoles = (JSONArray) jsonRoot.get("h");
        for (Iterator it = jsonHoles.iterator(); it.hasNext();) {
            Object jsonHole1 = it.next();
            JSONObject jsonHole = (JSONObject) jsonHole1;
            Character symbol = ((String) jsonHole.get("s")).charAt(0);
            int row = ((Long) jsonHole.get("r")).intValue();
            int column = ((Long) jsonHole.get("c")).intValue();
            this.getHole(row, column).setHoleColor(this.getHoleColor(symbol));
        }

        // Öltések fonalakhoz rendelése.
        JSONArray jsonWools = (JSONArray) jsonRoot.get("w");
        for (Iterator it = jsonWools.iterator(); it.hasNext();) {
            Object jsonWool1 = it.next();
            JSONObject jsonWool = (JSONObject) jsonWool1;
            Character colorSymbol = ((String) jsonWool.get("c")).charAt(0);
            Wool w = this.getWool(colorSymbol);
            JSONArray jsonStitches = (JSONArray) jsonWool.get("s");
            for (Iterator it2 = jsonStitches.iterator(); it2.hasNext();) {
                Object jsonStitch1 = it2.next();
                JSONObject jsonStitch = (JSONObject) jsonStitch1;
                Stitch s = this.getStitch(((Long) jsonStitch.get("r")).intValue(), ((Long) jsonStitch.get("c")).intValue());
                if (null != jsonStitch.get("i")) {
                    s.reverse();
                }
                s.addToWool(w);
            }
        }

        this.filePathName = filePathName;
        this.modified = false;
    }

    /**
     * Kanava átalakítása a szerver által értelmezhető formátumra.
     *
     * @return átalakított kanava
     */
    public CompiledKanava compile() {
        CompiledKanava result = new CompiledKanava();

        this.wools.values().stream().filter((w) -> !(' ' == w.getColor().getSymbol())).filter((w) -> !(!(0 < w.size()))).forEach((w) -> {
            result.add(w.toHolePath());
        });

        result.sort();
        result.storeTotalCount();

        return result;
    }

    /**
     * Szkript forráskódjának tárolása.
     *
     * @param script szkript forráskódja
     */
    public void setScriptSourceCode(String script) {
        this.script = script;
        this.toModified();
    }

    /**
     * Szkript forráskódjának lekérdezése.
     *
     * @return szkript forráskódja
     */
    public String getScriptSourceCode() {
        return this.script;
    }

}
